﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading;

namespace GoogleApi
{
    public class AutoCompleteRequest
    {
        #region LanguageName enum

        public enum LanguageName
        {
            Arabic,
            Basque,
            Bulgarian,
            Bengali,
            Catalan,
            Czech,
            Danish,
            German,
            Greek,
            English,
            EnglishAustralian,
            EnglishGreatBritain,
            Spanish,
            Farsi,
            Finnish,
            Filipino,
            French,
            Galician,
            Gujarati,
            Hindi,
            Croatian,
            Hungarian,
            Indonesian,
            Italian,
            Hebrew,
            Japanese,
            Kannada,
            Korean,
            Lithuanian,
            Latvian,
            Malayalam,
            Marathi,
            Dutch,
            NorwegianNynorsk,
            Norwegian,
            Oriya,
            Polish,
            Portuguese,
            PortugueseBrazil,
            PortuguesePortugal,
            Romansch,
            Romanian,
            Russian,
            Slovak,
            Slovenian,
            Serbian,
            Swedish,
            Tagalog,
            Tamil,
            Telugu,
            Thai,
            Turkish,
            Ukrainian,
            Vietnamese,
            ChineseSimplified,
            ChineseTraditional,
        }

        #endregion

        #region PlaceType enum

        public enum PlaceType
        {
            /// <summary>
            ///Geocode instructs the Place service to return only geocoding (address) results. Generally, you use this request to disambiguate results where the location specified may be indeterminate.
            /// </summary>
            Geocode,

            /// <summary>
            ///Establishment instructs the Place service to return only business results.
            /// </summary>
            Establishment,

            /// <summary>
            ///The (regions) type collection instructs the Place service to return any result matching the following types: locality, sublocality, postal_code, country, administrative_area1, administrative_area2
            /// </summary>
            Regions,

            /// <summary>
            ///The (cities) type collection instructs the Place service to return results that match either locality or administrative_area3.
            /// </summary>
            Cities
        }

        #endregion

        #region Properties

        /// <summary>
        /// Your Google Api Key
        /// </summary>
        public string ApiKey { get; private set; }

        /// <summary>
        /// Boolean stating whether request is originating from device that is GPS enabled. Defaults to true
        /// </summary>
        public bool Sensor { get; set; }

        /// <summary>
        /// Latitude for the originating location
        /// </summary>
        public double? Latitude { get; set; }

        /// <summary>
        /// Longitude for the originating location
        /// </summary>
        public double? Longitude { get; set; }

        /// <summary>
        /// The distance (in meters) within which to return Place results. 
        /// Note that setting a radius biases results to the indicated area, but may not fully restrict results to the specified area.
        /// </summary>
        public double? Radius { get; set; }

        /// <summary>
        /// Language to return the reponses in. English (en) is default.
        /// </summary>
        public LanguageName? Language { get; set; }


        /// <summary>
        /// The types of Place results to return.
        /// </summary>
        public PlaceType? Place { get; set; }

        /// <summary>
        /// A public property representing the current list of results. 
        /// </summary>
        public Predictions Results { get; private set; }

        /// <summary>
        ///  Currently, you can use components to filter by country. The country must be passed as a two character, ISO 3166-1 Alpha-2 compatible country code. 
        /// </summary>
        public String CountryComponent { get; set; }

        #endregion

        #region Fields

        private HttpWebRequest _request;
        private readonly Dictionary<LanguageName?, string> _languages;
        public static ManualResetEvent AllDone = new ManualResetEvent(false);
        private const int BufferSize = 1024;
        private const int DefaultTimeout = 2*30*250; // 30 second timeout

        #endregion

        #region Handlers

        public delegate void AcRequestCompletedHandler(object myObject, AutoCompleteEventArgs myArgs);

        public event AcRequestCompletedHandler AutoCompleteRequestCompleted;

        #endregion

        #region Private Methods

        // Abort the request if the timer fires.
        private static void TimeoutCallback(object state, bool timedOut)
        {
            if (!timedOut) return;
            var request = state as HttpWebRequest;
            if (request != null)
            {
                request.Abort();
            }
        }

        //use to pass correct language format to the google api
        private Dictionary<LanguageName?, string> LanguageCodeDicionary()
        {
            return new Dictionary<LanguageName?, string>
                       {
                           {LanguageName.Arabic, "ar"},
                           {LanguageName.Basque, "eu"},
                           {LanguageName.Bulgarian, "bg"},
                           {LanguageName.Bengali, "bn"},
                           {LanguageName.Catalan, "ca"},
                           {LanguageName.Czech, "cs"},
                           {LanguageName.Danish, "da"},
                           {LanguageName.German, "de"},
                           {LanguageName.Greek, "el"},
                           {LanguageName.English, "en"},
                           {LanguageName.EnglishAustralian, "en-AU"},
                           {LanguageName.EnglishGreatBritain, "en-GB"},
                           {LanguageName.Spanish, "es"},
                           {LanguageName.Farsi, "fa"},
                           {LanguageName.Finnish, "fi"},
                           {LanguageName.Filipino, "fil"},
                           {LanguageName.French, "fr"},
                           {LanguageName.Galician, "gl"},
                           {LanguageName.Gujarati, "gu"},
                           {LanguageName.Hindi, "hi"},
                           {LanguageName.Croatian, "hr"},
                           {LanguageName.Hungarian, "hu"},
                           {LanguageName.Indonesian, "id"},
                           {LanguageName.Italian, "it"},
                           {LanguageName.Hebrew, "iw"},
                           {LanguageName.Japanese, "ja"},
                           {LanguageName.Kannada, "kn"},
                           {LanguageName.Korean, "ko"},
                           {LanguageName.Lithuanian, "lt"},
                           {LanguageName.Latvian, "lv"},
                           {LanguageName.Malayalam, "ml"},
                           {LanguageName.Marathi, "mr"},
                           {LanguageName.Dutch, "nl"},
                           {LanguageName.NorwegianNynorsk, "nn"},
                           {LanguageName.Norwegian, "no"},
                           {LanguageName.Oriya, "or"},
                           {LanguageName.Polish, "pl"},
                           {LanguageName.Portuguese, "pt"},
                           {LanguageName.PortugueseBrazil, "pt-BR"},
                           {LanguageName.PortuguesePortugal, "pt-PT"},
                           {LanguageName.Romansch, "rm"},
                           {LanguageName.Romanian, "ro"},
                           {LanguageName.Russian, "ru"},
                           {LanguageName.Slovak, "sk"},
                           {LanguageName.Slovenian, "sl"},
                           {LanguageName.Serbian, "sr"},
                           {LanguageName.Swedish, "sv"},
                           {LanguageName.Tagalog, "tl"},
                           {LanguageName.Tamil, "ta"},
                           {LanguageName.Telugu, "te"},
                           {LanguageName.Thai, "th"},
                           {LanguageName.Turkish, "tr"},
                           {LanguageName.Ukrainian, "uk"},
                           {LanguageName.Vietnamese, "vi"},
                           {LanguageName.ChineseSimplified, "zn-CN"},
                           {LanguageName.ChineseTraditional, "zh-TW"},
                       };
        }

        private void RespCallback(IAsyncResult asynchronousResult)
        {
            try
            {
                // State of request is asynchronous.
                var myRequestState = (RequestState) asynchronousResult.AsyncState;
                var myHttpWebRequest = myRequestState.Request;
                myRequestState.Response = (HttpWebResponse) myHttpWebRequest.EndGetResponse(asynchronousResult);

                // Read the response into a Stream object.
                var responseStream = myRequestState.Response.GetResponseStream();
                myRequestState.StreamResponse = responseStream;


                responseStream.BeginRead(myRequestState.BufferRead, 0, BufferSize, ReadCallBack, myRequestState);
                return;
            }
            catch (WebException e)
            {
                Console.WriteLine("\nRespCallback Exception raised!");
                Console.WriteLine("\nMessage:{0}", e.Message);
                Console.WriteLine("\nStatus:{0}", e.Status);
            }
            AllDone.Set();
        }

        private void ReadCallBack(IAsyncResult asyncResult)
        {
            try
            {
                var myRequestState = (RequestState) asyncResult.AsyncState;
                var responseStream = myRequestState.StreamResponse;

                var result = Deserialize(responseStream, typeof (Predictions)) as Predictions;


                //raise completed event, passing deserialized results data as argument. Useful for non binding scenarios, such as WP7 autocomplete box where binding
                //does not support an observable collection
                var myArgs = new AutoCompleteEventArgs {Results = result, ErrorCode = result.status};
                AutoCompleteRequestCompleted(this, myArgs);


                //set results property for binding
                Results = result;
            }
            catch (WebException e)
            {
                Console.WriteLine("\nReadCallBack Exception raised!");
                Console.WriteLine("\nMessage:{0}", e.Message);
                Console.WriteLine("\nStatus:{0}", e.Status);
            }
            catch (Exception e)
            {
                Console.WriteLine("\nReadCallBack Exception raised!");
                Console.WriteLine("\nMessage:{0}", e.Message);
            }
            AllDone.Set();
        }

        private static object Deserialize(Stream streamObject, Type serializedObjectType)
        {
            if (serializedObjectType == null || streamObject == null)
                return null;

            try
            {
                var ser = new DataContractJsonSerializer(serializedObjectType);
                return ser.ReadObject(streamObject);
            }
            catch (Exception e)
            {
                Console.WriteLine("\nDeserialization Exception raised!");
                Console.WriteLine("\nMessage:{0}", e.Message);

                return null;
            }
        }

        private string PlaceString(PlaceType? type)
        {
            switch (type)
            {
                case PlaceType.Geocode:
                    return "geocode";

                case PlaceType.Establishment:
                    return "establishment";

                case PlaceType.Regions:
                    return "(regions)";
                case PlaceType.Cities:
                    return "(cities)";
                default:
                    return "";
            }
        }

        private void AddOptionalData(StringBuilder str)
        {
            //optional data
            if (CountryComponent != null)
            {
                str.Append(String.Format("&components=country:{0}", CountryComponent));
            }
            if (Place != null)
            {
                str.Append("&types=" + HttpUtility.UrlEncode(PlaceString(Place)));
            }
            if (Latitude != null && Longitude != null)
            {
                str.Append(String.Format("&location={0},{1}", Latitude, Longitude));
            }
            if (Language != null)
            {
                str.Append(String.Format("&language={0}", _languages[Language]));
            }

            if (Radius != null)
            {
                str.Append(String.Format("&radius={0}", Radius));
            }
        }

        #endregion

        /// <summary>
        /// Constructor to set defaults
        /// </summary>
        public AutoCompleteRequest(string apikey)
        {
            //defaults
            Sensor = true;
            ApiKey = apikey;

            //set languages
            _languages = LanguageCodeDicionary();
        }


        /// <summary>
        /// Start an Autocomplete request. A
        /// </summary>
        /// <param name="input"> The text string on which to search. The Place service will return candidate matches based on this string and order results based on their perceived relevance.</param>
        public void StartAutoCompleteAsync(string input)
        {
            try
            {
                //setup URI String for request
                var data = new StringBuilder();
                data.Append("json?");
                data.Append("input=" + HttpUtility.UrlEncode(input));
                data.Append(Sensor ? String.Format("&sensor={0}", "true") : String.Format("&sensor={0}", "false"));
                AddOptionalData(data);
                data.Append(String.Format("&key={0}", ApiKey));


                //setup webrequest
                var finalUri = "https://maps.googleapis.com/maps/api/place/autocomplete/" + data;
                _request = WebRequest.Create(finalUri) as HttpWebRequest;

                // Set type to POST  
                _request.Method = "POST";
                _request.ContentType = "application/x-www-form-urlencoded";


                var myRequestState = new RequestState {Request = _request};

                // Start the asynchronous request.
                var result = _request.BeginGetResponse(RespCallback, myRequestState);

                // this line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted
                ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, TimeoutCallback,
                                                       _request, DefaultTimeout, true);

                // The response came in the allowed time. The work processing will happen in the 
                // callback function.
                AllDone.WaitOne();

                // Release the HttpWebResponse resource.
                myRequestState.Response.Close();
            }

            catch (WebException e)
            {
                Console.WriteLine("\nRespCallback Exception raised!");
                Console.WriteLine("\nMessage:{0}", e.Message);
                Console.WriteLine("\nStatus:{0}", e.Status);
            }
            catch (Exception e)
            {
                Console.WriteLine("\nRespCallback Exception raised!");
                Console.WriteLine("\nMessage:{0}", e.Message);
            }
        }
    }

    public class RequestState
    {
        // This class stores the State of the request.
        private const int BufferSize = 1024;
        public StringBuilder RequestData;
        public byte[] BufferRead;
        public HttpWebRequest Request;
        public HttpWebResponse Response;
        public Stream StreamResponse;

        public RequestState()
        {
            BufferRead = new byte[BufferSize];
            RequestData = new StringBuilder("");
            Request = null;
            StreamResponse = null;
        }
    }
}